package server;

import java.util.ArrayList;
import java.util.Random;

import tools.*;

public class Game implements Runnable
{
	private final int MIN_PLAYING_PLAYERS = 2;
	private final int DELAY = 1000;
	ServerBadugi server;
	GameState gameState;
	Deck deck;
	Thread thread;
	
	public Game(ServerBadugi server)
	{
		this.server = server;
		gameState = new GameState(server.numberOfPlayers, server.smallBlind, server.bigBlind, server.numberOfChips);
	}
	
	void setThread(Thread thread)
	{
		this.thread = thread; 
	}
	
	void addPlayer(ClientService cs)
	{
		int id = server.getFreeID();
		cs.id = id;
		server.connectedPlayers.add(cs);			
		cs.player = gameState.addPlayer(id);
	}

	void removePlayer(ClientService cs)
	{
		/*
		 * gracz nie zostal zarejestrowany, a wiec nie trzeba go usuwac
		 * */
		if (cs.id == 0) return;
		
		server.connectedPlayers.remove(cs);
		this.returnCards(cs.id);
		gameState.removePlayer(cs.id);
		server.returnFreeID(cs.id);
	}
	
	private void setPlayersPlaying()
	{
		synchronized(server)
		{
			for(Player player: gameState.players)
				if (player.isWaiting()) player.setPlaying();
		}
	}

	private void setPlayersWaiting()
	{
		synchronized(server)
		{
			for(Player player: gameState.players)
				if (player.isPlayer()) player.setWaiting();
		}
	}

	/*
	 * METODY OBSLUGUJACE KOMUNIKACJE Z KLIENTAMI
	 * */

	private void sendToEveryone(Protocol protocol)
	{
		this.sendToEveryone(protocol, 1);		
	}
	
	private void sendToEveryone(Protocol protocol, int delay)
	{
		synchronized(server)
		{
			for(ClientService cs: server.connectedPlayers)
			{
				if (cs.player.isPlayer())
				{
					cs.sendResponse(protocol);
				}
			}
		}
		try
		{
			Thread.sleep(delay * DELAY);
		}
		catch (InterruptedException e)
		{
			e.printStackTrace();
		}		
	}
		
	private void SendToPlayer(int playerID, Protocol protocol)
	{
		ClientService cs = this.getClientService(playerID);
		synchronized(server)
		{
			cs.sendResponse(protocol);
		}
		try
		{
			Thread.sleep(DELAY);
		}
		catch (InterruptedException e)
		{
			e.printStackTrace();
		}
	}
	
	private Protocol SendToPlayerAndWait(int playerID, Protocol protocol)
	{	
		ClientService cs = this.getClientService(playerID);
		Protocol p;
		synchronized(server)
		{
			p = cs.sendResponseAndWait(protocol);
		}
		try
		{
			Thread.sleep(DELAY);
		}
		catch (InterruptedException e)
		{
			e.printStackTrace();
		}
		return p;
	}
	
	private int countPlayingPlayers()
	{
		return gameState.countPlayingPlayers();
	}
	
	/* GLOWNA PETLA */
	@Override
	public void run()
	{
		/*
		 * Glowna petla gry
		 * */
		while (true)
		{
			/* CZEKANIE NA GRACZY */
			gameState.setWaiting();
			
			while (this.countWaitingPlayers() < server.numberOfPlayers)
			{
				sendToEveryone(Protocol.WAITING_FOR_PLAYERS);
			}
			boolean firstGame = true;
			/* oznacz czekajacych jako grajacych */
			setPlayersPlaying();
			/* wyzerowanie puli */
			gameState.startGame();
			
			/* ROZGRYWKA - dopoki jest z kim grac */			
			while (this.arePlayersToGame())
			{
				/*
				 * Jest wymagana liczba graczy
				 * 1. etap - losowanie kart i rozdanie kart, rozdanie buttonow:
				 *           - dealer button
				 *           - smallBlind
				 *           - BigBlind 
				 * */
				
				/* ustaw pierwsza runda licytacji/wymiany kart 
				 * wyczysc liste zwyciezcow
				 * */
				this.gameState.startRounds();
				prepareGame(firstGame);
				firstGame = false;
				/* reset najwyzszej stawki */
				this.gameState.setHighestBetInRound(0);
				
				/* SEND - informacja do klientow o rozpoczeciu gry */
				this.sendToEveryone(Protocol.START_GAME);
				
				/* RUNDY - dopoki */
				while (this.gameState.getNumberOfRound() <= 4 && this.arePlayersToGame())
				{
					/* ustaw stan gry */
					if (this.gameState.getNumberOfRound() < 4)
						this.gameState.setBettingRound();
					else
						this.gameState.setFinalRound();
					
					/* reset ID gracza ktory podbil stawke oraz reset najwyzszej stawki */
					this.gameState.setHighestBetInRoundPlayerID(0);
					
					/* dodalem to bo chyba niechcacy wczesniej usunalem - nie pojawial sie przycisk BET a wczesniej byl */
					this.gameState.setHighestBetInRound(0);
					
					boolean isFirstBetInRound = true;
					
					/* gracze na Small i Big */
					Player pSmall = this.gameState.players.get(this.gameState.getSmallBlindPlayerID());
					Player pBig = this.gameState.players.get(this.gameState.getBigBlindPlayerID()); 

					
					/* jesli pierwsza runda to przymusowe zagranie big i small */
					if (this.gameState.getNumberOfRound() == 1)
					{
						/* obowiazkowe zagrywki */
						pSmall.betSmallBlind();
						pBig.betBigBlind();
						
						/* dodanie do puli*/
						this.gameState.setPot(this.gameState.getPot() + pSmall.getBet());
						this.gameState.setPot(this.gameState.getPot() + pBig.getBet());
						
						/* jesli pierwsza runda to ustawienie najwyzszej stawki na gracza rozpoczynajcaego, 
						 * a kwote na Big Blind
						 *  */
						//isFirstBetInRound = true;
						gameState.setHighestBetInRound(pBig.getBet());
						
						/* rozpoczyna gracz nastepny po Big Blind */
						Player pNext = this.gameState.players.get(this.nextPlayerIDToBetting(pBig.getID()));
						
						if (!pNext.isAllIn())
							this.gameState.setTurningPlayer(pNext.getID());
						else
						{
							int nextPlayerID = this.nextPlayerIDToBetting(pNext.getID());
							/* jesli wszysycy zagrali allin to nie ma kto licytowac */
							if (nextPlayerID == pNext.getID())
								/* i w takim przypadku ustawiamy 0 poniewaz petal licytacji nie moze sie uruchomic */
								this.gameState.setTurningPlayer(0);
							else
								this.gameState.setTurningPlayer(nextPlayerID);	
						}
					}
					else
					{
						/* w nastepnych rundach rozpoczyna gracz ze Small Blind jesli nie zagral All-in */
						if (!pSmall.isAllIn())
							this.gameState.setTurningPlayer(pSmall.getID());
						else
						{
							int nextPlayerID = this.nextPlayerIDToBetting(pSmall.getID());
							/* jesli wszysycy zagrali allin to nie ma kto licytowac */
							if (nextPlayerID == pSmall.getID())
								/* i w takim przypadku ustawiamy 0 poniewaz petal licytacji nie moze sie uruchomic */
								this.gameState.setTurningPlayer(0);
							else
								this.gameState.setTurningPlayer(nextPlayerID);
						}
					}
					
					/* PETLA LICYTACJI */
					while (this.gameState.getHighestBetInRoundPlayerID() != this.gameState.getTurningPlayer() && this.arePlayersToGame())
					{
						/* SEND - uaktualnienie stanu gry u klientow */
						this.sendToEveryone(Protocol.REFRESH);
						
						/* SEND AND WAIT - stawiaj i jesli przeblies to ustaw ID najwyzszej stawki */
						bid(this.gameState.getTurningPlayer());
						/* pobranie player dopiero po wywloaniu BID poniewaz po odbiorze komuniaktu 
						 * od klienta jest zawsze adres player w tablicy jest zmieniany */
						Player player = this.gameState.players.get(this.gameState.getTurningPlayer());
						
						if (isFirstBetInRound)
						{
							isFirstBetInRound = false;
							gameState.setHighestBetInRound(player.getInGameChips());
							gameState.setHighestBetInRoundPlayerID(player.getID());
						}
						
						/* nastepny gracz nextID */
						int nextPlayerID = this.nextPlayerIDToBetting(player.getID());
						if (nextPlayerID == player.getID())
							/* licytacja wraca do tego samego gracza - nie ma sensu  */
							break;
						else
							/* OK */
							this.gameState.setTurningPlayer(nextPlayerID);
					}
					
					/* kolko licytacji sie zakonczylo tzn. ID najwyzszego gracza = ID gracza ktory mial robic ruch */
					
					System.out.println("Przejscie do wymiany kart. nr rundy: "+this.gameState.getNumberOfRound());
					System.out.println("Przejscie do wymiany kart. czy sa gracze do wymiany?: "+this.arePlayersToGame());
					
					/* PETLA WYMIANY KART - czekanie na rekacje gracza! */
					
					if (this.gameState.getNumberOfRound() < 4 && this.arePlayersToGame())
					{
						System.out.println("Wymiana kart START");
						/* wymiane kart rozpoczynamy od dealer button ID. nie bylo wymagania wiec tak zrobilem */
						/* gracz Dealer Button */
						Player pDB = this.gameState.players.get(this.gameState.getDealerButtonPlayerID());
						
						if (pDB.isPlaying())
							this.gameState.setTurningPlayer(pDB.getID());
						else
						{
							/* jesli Dealer button dal FOLD do odpada z wymiany kart i szukamy nastepnego */
							int nextPlayerID = this.nextPlayerID(pDB.getID());
							
							if (nextPlayerID == pDB.getID())
								/* jesli nie ma nastepnego to ustawiamy 0 */
								this.gameState.setTurningPlayer(0);
							else
								this.gameState.setTurningPlayer(nextPlayerID);
						}
						
						System.out.println("Wymiana kart rozpoczyna gracz: " + this.gameState.getTurningPlayer());
						
						/* jesli nie ma kto wymienic kart to nie uruchamiam petli wymiany */
						if (this.gameState.getTurningPlayer() != 0)
						{
							/* ustaw stan gry */
							this.gameState.setExchangeRound();
							
							do
							{
								/* SEND - uaktualnienie stanu gry u klientow */
								this.sendToEveryone(Protocol.REFRESH);
	
								/* SEND AND WAIT - wymiana kart przez gracza */	
								exchangeCards(this.gameState.getTurningPlayer());
								
								/* nastepny gracz nextID */
								this.gameState.setTurningPlayer(this.nextPlayerID(this.gameState.getTurningPlayer()));
								
							} while (this.gameState.getDealerButtonPlayerID() != this.gameState.getTurningPlayer() && this.arePlayersToGame());
						}
					}
					/* zwieksz licznik rund */
					this.gameState.setNumberOfRound(this.gameState.getNumberOfRound()+1);
				}
				/* ustawienie na 4 nr rundy dla czystosci numeracji, to ustawienie nie ma znaczenia dla algorytmu */
				this.gameState.setNumberOfRound(4);
				/* reset gracza wykonujacego ruch */
				this.gameState.setTurningPlayer(0);

				/* pokazanie kart
				 * ustalenie wygranego i podzial puli
				 * oznaczenie graczy 
				 * */
				this.gameState.setEndGame();
				/* SEND - koncowe wyslanie stanu gry i wynikow  */
				endGame();
				
				/* ustawienie wszysktim graczom posiadajacym zetony stanu PLAYING, 
				 * nowi ktorzy dolaczyli w czasie gry tez dostana 
				 * Gracze ktorzy nie maja zetonow zostaja wyrzuceni komenda KICK
				 * */
				verifyPlayers();
			}
			
			/* zostal jeden gracz - musi poczekac */
			this.setPlayersWaiting();
		}
	}
	
	private void verifyPlayers()
	{
		/* ustawienie wszysktim graczom posiadajacym zetony stanu PLAYING, 
		 * nowi ktorzy dolaczyli w czasie gry tez dostana 
		 * Gracze ktorzy nie maja zetonow zostaja wyrzuceni komenda KICK
		 * */
		for(Player p : gameState.players)
		{
			/* jesli gracz ma wystarczajaca ilosc zetonow do gry  */
			if (!p.isEmptySlot() && p.hasTotalChipsToGame()) 
			{
				p.setPlaying();
				p.setAllIn(false);
			}
			else if (!p.isEmptySlot())
			{
				/* jesli nie to KCIK */
				System.out.println("verifyPlayers() - player KICK: " + p.getID());
				this.SendToPlayer(p.getID(), Protocol.KICK);
			}
		}
	}

	private void endGame()
	{
		/* pokazanie kart 
		 * ustalenie wygranego i podzial puli
		 * 
		 * */
		
		Player bestPlayer=null;
		/* szukanie najlepszego rozkladu wsrod graczy */
		for(Player player : this.gameState.players)
			if (player.isPlaying())
			{
				/* przy okazji wyczyszczenie statusu gracza */
				player.setStatusGame("");
				if ((bestPlayer == null) || (player.getHand().compareTo(bestPlayer.getHand()) > 0))
					bestPlayer = player;
				
				System.out.println("End game. Karty gracza "+ player.getID() + " " + player.getHand().getCards().toString());
			}

		/* wpisanie graczy z najlepszym rozkladem na liste zwyciezcow */
		for(Player player : this.gameState.players)
			if (player.isPlaying())
				if (player.getHand().compareTo(bestPlayer.getHand()) == 0)
				{
					this.gameState.addWinner(player);
					/* ustawienie stusu na wypadek remisu, jesli bedzie tylko jeden zwyciezca to status zostanie nadpisany */
					player.setStatusGame("Unlucky winner :(");
					System.out.println("Najlepszy rozklad: gracz "+ player.getID() + " " + player.getHand().getCards().toString());
				}
		
		if (this.gameState.getWinner() != null && !this.gameState.isDraw())
		{
			this.gameState.getWinner().setStatusGame("Winner!");
			this.gameState.sharePot();
		}
		
		/* wstrzymaj gre na czas 3 razy dluzszy niz normalnie (3 * DELAY) */
		this.sendToEveryone(Protocol.END_GAME, 3);
		this.returnCards();
	}

	private void bid(int playerID)
	{
		Protocol answer;
		/* obsluga licytacji przez gracza "p" */
		answer = this.SendToPlayerAndWait(playerID, Protocol.BID);

		Player p = this.gameState.players.get(playerID);
		
		if (answer == Protocol.CHECK )
		{
			p.setStatusGame("Check");
		}
		else if (answer == Protocol.CALL )
		{
			p.setStatusGame("Call: " + p.getBet());
		}
		else if (answer == Protocol.BET )
		{
			p.setStatusGame("Bet: " + p.getBet());
		}
		else if (answer == Protocol.RAISE )
		{
			p.setStatusGame("Raise: " + p.getBet());
		}
		else if (answer == Protocol.ALL_IN )
		{
			p.setStatusGame("All-in: " + p.getBet());
		}
		else if (answer == Protocol.FOLD )
		{
			p.setStatusGame("Fold");
			return;
		}
		else if (answer == Protocol.QUIT)
		{
			/* inni gracze wyszli i serwer to wykryl i wyslal te komende */
			return;
		}
		
		gameState.setPot(gameState.getPot() + p.getBet());

		if (p.getInGameChips() > gameState.getHighestBetInRound())
		{
			gameState.setHighestBetInRound(p.getInGameChips());
			gameState.setHighestBetInRoundPlayerID(p.getID());
		}
		
		p.setBet(0);
	}

	private void exchangeCards(int playerID)
	{
		/* obsluga wymiany kart przez gracza "p" */
		Protocol answer;
		answer = this.SendToPlayerAndWait(playerID, Protocol.EXCHANGE_CARDS);
		
		Player p = this.gameState.players.get(playerID);
		
		/* wymienil karty */
		if (answer == Protocol.EXCHANGED_CARDS)
		{
			ArrayList<Card> newCards = new ArrayList<Card>(p.getNumberExchangedCards());
			/* wymiana kart podeslanych przez klienta */
			p.setStatusGame("Exchanged "+ p.getNumberExchangedCards() +" cards");
			/* nowe karty z talii */
			for(int i=0; i<p.getNumberExchangedCards(); i++)
				newCards.add(i, this.deck.getCard());

			/* podmiana kart */
			p.swapCards(newCards);
			
			/* zwrot wymienianych kart do trash */
			deck.returnCards(p.returnExchangedCards());
			
			System.out.println("Gracz "+playerID+" wymienil "+ p.getNumberExchangedCards() +" kart");
		}
		else if (answer == Protocol.NO_EXCHANGE )
		{
			p.setStatusGame("No exchange cards");
			System.out.println("Gracz "+playerID+" nie wymienil kart");
		}
	}
	
	/* pobranie watku obslugujacego polaczenie z graczem */
	private ClientService getClientService(int playerID)
	{
		ClientService dummy = null;
		for(ClientService cs : server.connectedPlayers)
			if (cs.id == playerID)
				return cs; 
		return dummy;
	}
	
	
	boolean arePlayersToGame()
	{
		return (this.countPlayingPlayers() >= MIN_PLAYING_PLAYERS && this.isMoreThanOnePlayerWithChips());
	}
	
	private int countWaitingPlayers()
	{
		return (this.gameState.countWaitingPlayers());
	}

	/* czynnosci przed rozgrywka */
	private void prepareGame(boolean firstGame)
	{
		if (firstGame)
		{
			/* nowa talia kart */
			deck = new Deck();
		}
		preparePlayers();
		/* rozdaj karty */
		distributeCards();
		/* przydziel Dealer Button oraz Big Blind Small Blind */
		distributeDealerButton(firstGame);
	}
	
	private void preparePlayers()
	{
		for(Player p : gameState.players)
		{
			/* wyzerowanie stawek bet, InGameChips */
			if (p.isPlaying()) p.startGame();
		}
	}

	private void distributeDealerButton(boolean firstGame)
	{		
		if (firstGame)
		{
			/* jesli grana pierwsza rozgrywka w grze to losuj ID gracza */
			ArrayList<Integer> ids = new ArrayList<Integer>();
			
			/* pobierz ID graczy */
			for(Player p : gameState.players)
				if (p.isPlaying()) 
					ids.add(p.getID());

			/* losuj ID gracza z dealer button */
			Random random = new Random();
			/* z tablicy z IDs wylosuj indeks z ktorego pobierzemy ID */
			int whichID = random.nextInt(ids.size()-1);
			/* ID wylosowanego gracza */;
			gameState.setDealerButtonPlayerID(ids.get(whichID));
		}
		else
		{
			/* jesli nastepne rozgrywki to nastepny gracz */
			/* najpierw usun znacznik poprzedniemu graczowi, jesli jeszzce on jest na serwerze ;) */
			Player pDB = gameState.players.get(gameState.getDealerButtonPlayerID());
			if (pDB.isPlaying()) pDB.setNoFunction();
			gameState.setDealerButtonPlayerID(nextPlayerIDToBetting(gameState.getDealerButtonPlayerID()));
		}
		
		System.out.println("Przydzielono Dealer Button: gracz " + gameState.getDealerButtonPlayerID());
		
		/* ustaw graczowi zmienna dealearbutton na true */
		gameState.players.get(gameState.getDealerButtonPlayerID()).setDealerButton();
		
		/* ustaw ID gracza na small blind */
		int smallBlindID = nextPlayerIDToBetting(gameState.getDealerButtonPlayerID());
		/* najpierw usun poprzedniemu znacznik small blind */
		Player pSB = gameState.players.get(gameState.getSmallBlindPlayerID());
		if (pSB.isPlaying()) pSB.setNoSmallBlind();
		gameState.setSmallBlindPlayerID(smallBlindID);
		gameState.players.get(smallBlindID).setSmallBlind();
		
		System.out.println("Przydzielono Small Blind: gracz " + smallBlindID);
		
		/* ustaw ID gracza na big blind */
		int bigBlindID = nextPlayerIDToBetting(smallBlindID);
		/* najpierw usun poprzedniemu znacznik big blind */
		Player pBB = gameState.players.get(gameState.getBigBlindPlayerID());
		if (pBB.isPlaying()) pBB.setNoBigBlind();
		
		gameState.setBigBlindPlayerID(bigBlindID);
		gameState.players.get(bigBlindID).setBigBlind();
		
		System.out.println("Przydzielono Big Blind: gracz " + bigBlindID);
	}

	private int nextPlayerID(int id)
	{
		int searchingID = ((id+1) % this.gameState.players.size());
		while (searchingID != id)
		{
			if (this.gameState.players.get(searchingID).isPlaying())
				break;
			/* nastepne ID liczone jako reszta z dzielenia przez liczbe slotow */
			searchingID = ((searchingID+1) % this.gameState.players.size());
		}
		return searchingID;
	}
	
	private int nextPlayerIDToBetting(int id)
	{
		int searchingID = ((id+1) % this.gameState.players.size());
		while (searchingID != id)
		{
			if (this.gameState.players.get(searchingID).isPlaying() && !this.gameState.players.get(searchingID).isAllIn())
				break;
			/* nastepne ID liczone jako reszta z dzielenia przez liczbe slotow */
			searchingID = ((searchingID+1) % this.gameState.players.size());
		}
		return searchingID;
	}

	private void distributeCards()
	{		
		for(Player p : this.gameState.players)
			if (p.isPlaying())
			{
				ArrayList<Card> cards = new ArrayList<Card>();
				for(int i=0; i<4; i++)
					cards.add(i, deck.getCard());
				p.takeCards(cards);
			}
		
		System.out.println("Rozdano karty");
	}
	
	private void returnCards()
	{
		for(Player p : this.gameState.players)
			returnCards(p);
	}
	
	private void returnCards(Player p)
	{
		/* jak ma karty niech odda */
		if (p.getHand() != null)
			deck.returnCards(p.returnCards());
	}
	
	private void returnCards(int playerID)
	{
		returnCards(this.gameState.getPlayer(playerID));
	}
	
	private boolean isMoreThanOnePlayerWithChips()
	{
		int counter = 0;
		for(Player p : this.gameState.players)
			if (p.isPlaying() && (p.isAllIn() || p.getTotalChips() > 0)) counter++;
		
		return (counter > 1);
	}
	
}
